package org.fjsei.yewu.util;

import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.Item;
import io.minio.messages.Retention;
import io.minio.messages.RetentionMode;
import jakarta.annotation.Resource;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import md.system.User;
import me.desair.tus.server.upload.UploadInfo;
import org.fjsei.yewu.config.MinioConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

/** minIO 分布式存储。
 *  TLS配置   https://www.hxstrive.com/subject/minio/668.htm
 * 安全考虑： 公用可下载的； minIO官方文档 https://min.io/docs/minio/linux/index.html
 * MinIO管理网站和我后端都用了token名字cookie冲突了，只好用两个不同浏览器来回避问题。
 * 设置永久访问，桶设置为公共永久可下载：  https://zhuanlan.zhihu.com/p/611761457
 * 对比看 SeaweedFS 的安全性不好。但 MinIO 的扩展性稍差,可承载规模稍小。文件集群和每个web后端一对一配套模式。
 * 扩容方案MinIO:  https://blog.csdn.net/songmulin/article/details/129070577
 * 扩容经验  https://blog.csdn.net/m0_64363449/article/details/132634832
 * 部署 etcd , 联邦扩容,桶名字路由映射集群；  https://zhuanlan.zhihu.com/p/558896919
 * 问了AI解决问题，手机无法访问minio，需确保MinIO的执行文件（如minio.exe）被正确添加到windows安全设置的白名单中。
 * */
@Slf4j
@Component
public class MinioUtil {
    @Autowired
    private MinioConfig prop;
    @Resource
    private MinioClient minioClient;
    SimpleDateFormat PathSdf = new SimpleDateFormat("/yyyyMM/ddHH/");
    /**
     * 查看存储bucket是否存在
     * @return boolean
     */
    public Boolean bucketExists(String bucketName) {
        Boolean found;
        try {
            found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return found;
    }
    public Boolean ossIsHealth() {
        return bucketExists(prop.getBucketName());
    }
    /** 一般网站创建就行了；
     *  创建存储bucket
     * @return Boolean
     */
    public Boolean makeBucket(String bucketName) {
        try {
            if (!bucketExists(bucketName)) {
                minioClient.makeBucket(MakeBucketArgs.builder()
                        .bucket(bucketName)
                        .build());
                String oldplstr= minioClient.getBucketPolicy(GetBucketPolicyArgs.builder().bucket(bucketName).build());
                //不能乱改这个的  "Version": "2012-10-17",
                String policyJson ="{\n" +
                        "    \"Version\": \"2012-10-17\",\n" +
                        "    \"Statement\": [\n" +
                        "        {\n" +
                        "            \"Action\": [\n" +
                        "                \"s3:GetObject\"\n" +
                        "            ],\n" +
                        "            \"Effect\": \"Allow\",\n" +
                        "            \"Principal\": {\n" +
                        "                \"AWS\": [\n" +
                        "                    \"*\"\n" +
                        "                ]\n" +
                        "            },\n" +
                        "            \"Resource\": [\n" +
                        "                \"arn:aws:s3:::" + bucketName + "/*\"\n" +
                        "            ]\n" +
                        "        }\n" +
                        "    ]\n" +
                        "}";
                minioClient.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucketName).config(policyJson).build());
                log.info("buckets：【{}】,创建[readwrite]策略成功！", bucketName);
            } else {
                log.info("minio bucket->>>【{}】already exists", bucketName);
            }
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
    /** 看看
     */
    public Boolean bucketPolicy(String bucketName) {
        try {
            if(bucketExists(bucketName)) {
                String oldplstr= minioClient.getBucketPolicy(GetBucketPolicyArgs.builder().bucket(bucketName).build());
                log.info("buckets：策略 {}", oldplstr);
            } else {
                log.info("minio bucket->>>【{}】not exists", bucketName);
            }
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
    /**
     * 删除存储bucket
     * @return Boolean
     */
    public Boolean removeBucket(String bucketName) {
        try {
            minioClient.removeBucket(RemoveBucketArgs.builder()
                    .bucket(bucketName)
                    .build());
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
    /**
     * 获取全部bucket
     */
    public List<Bucket> getAllBuckets() {
        try {
            List<Bucket> buckets = minioClient.listBuckets();
            return buckets;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**把TUS上传后的文件 转移给 分布式存储集群MinIO去。
     * getMetadata()元数据编码  https://github.com/tus/TUSKit/issues/147    视频流不需要知道大小的；
     *  Create object ends with '/' (also called as folder or directory).
     *  minioClient.putObject(PutObjectArgs.builder().bucket("my-bucketname").object("path/to/").stream(
     *              new ByteArrayInputStream(new byte[] {}), 0, -1).build());
     *上传不设置.retention(new Retention(RetentionMode.COMPLIANCE,endLive))的做删除才会立刻删除掉实际文件的。 加了不行，过期了就算执行删除也不会自动删除实际存储的？
     * 上一次失败的情况，HEAD消息执行可能进入这里的。
     * 真正上传完成的RequestMethod.PATCH消息近进入这里: 把本地TUS路径的倒腾给minIO系统;
     * minIO常用的元数据字段：business eid liveDays author filename,但是Content-Type会自动加的；
     * */
    public String uploadForTUS(InputStream file, UploadInfo uploadInfo, User who) {
        String format = PathSdf.format(new Date());
        String objectName= format + uploadInfo.getId().toString();
        try {
            Map<String, String> metas=uploadInfo.getMetadata();
            String liveDays=metas.getOrDefault("liveDays","1");
            long lvdays=Long.parseLong(liveDays);
            ZonedDateTime endLive = ZonedDateTime.now().plusDays(lvdays);     //保留存储天数=1天，确保不能被桶的有效期策略清理掉。
            metas.put("author",who.getUsername());
            //前端过来的没必要都保存minIO，删除没用的，保留有用的 eid business 作为搜索标签。
            metas.remove("liveDays");
            metas.remove("Filetype");
            metas.remove("Name");
            metas.remove("Type");
            metas.remove("Relativepath");
            StatObjectResponse oldStat=this.statObject(prop.getBucketName(), objectName, null);
            if(null!=oldStat)   throw new RuntimeException("居然重名:"+objectName);     //万一它已经存在的对象
            //要设置 Object Locking=true；才能做对象生存期设置。
            //.retention  X-Amz-Object-Lock-Retain-Until-Date  锁 不能自动删除   ; legalHold;
            PutObjectArgs objectArgs = PutObjectArgs.builder().bucket(prop.getBucketName()).object(objectName)
                    .stream(file, uploadInfo.getLength(), -1).contentType(uploadInfo.getFileMimeType())
                    .retention(new Retention(RetentionMode.COMPLIANCE,endLive))
                    .userMetadata(metas)
                    .build();
            //文件名称相同会覆盖  #本地文件用 file.transferTo(File localObjectFile);
            minioClient.putObject(objectArgs);    //builder.build()
        } catch (Exception e) {
            log.error("MinIO上传失败 - 文件名: {}, 错误: {}", uploadInfo.getFileName(), e.getMessage());
            // 检查是否是连接相关的异常
            if (e.getMessage().contains("Connection") || e.getMessage().contains("Connect") ||
                    e.getMessage().contains("refused") || e.getMessage().contains("timeout")) {
                throw new RuntimeException("MinIO服务连接失败，请检查集群状态", e);
            } else if (e.getMessage().contains("No space") || e.getMessage().contains("disk full")) {
                throw new RuntimeException("存储空间不足", e);
            } else {
                throw new RuntimeException("MinIO存储失败: " + e.getMessage(), e);
            }
        }
        //前端,数据库保存整个字符串http://192.168.171.3:9000/ywmast/202310/13/dbe3d4d5-4a5c-46b5-9e58-9234e50b4b1e 可以“..桶+objectName”直接访问文件
//        String url=(null==prop.getWeburlendp()? prop.getEndpoint() : prop.getWeburlendp())+prop.getBucketName()+objectName; 需要保存bucketName存储桶名称，前端对端点用配置映射。
        String url= prop.getBucketName()+objectName;        //不能是中文路径名的。
        StatObjectResponse newStat=this.statObject(prop.getBucketName(), objectName, null);
        if(null==newStat)   throw new RuntimeException("MinIO居然丢失:"+objectName);
        return url;
    }


    /** 临时的previewUrl,非常冗长,且设置有的访问时效,默认7天!
     * 预览图片；== 直接minIO集群原生提供的下载 临时URL 模式的。
     * @param fileName =文件对象的id；
     * @return
     * 【可能性】minio生成临时凭证accessKey，secretKey，sessionToken返给前端，让前端不经过后端倒腾一手的浏览器客户端可直接上传文件给MinIO存储系统的模式。临时凭证的默认过期时间是1个小时；
     * 想永久访问，通过http://localhost:xx/bucketName/image.jpg 这种方式去访问,需要对bucket进行设置:
     */
    public String preview(String fileName){
        // 查看文件地址   getBucketName=桶名字：应是后端掌握配置的，或许可不定期地可以增加变更为新的桶名字。
        GetPresignedObjectUrlArgs build = new GetPresignedObjectUrlArgs().builder().bucket(prop.getBucketName()).object(fileName).method(Method.GET).build();
        try {
            //取临时链接（client.getPresignedObjectUrl），只有七天！
            String url = minioClient.getPresignedObjectUrl(build);
            return url;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /** #实际上没有用到！
     * 是直接点击下载模式？ 后端作为中间网关再次复制一遍文件内容的模式！； #不是图片<image></> URL: 永久访问Url模式的。
     * 文件下载: 直接读取minIO文件的数据包，然后后端服务器再去转给服务Controller也即API，前端依赖这个API链接来下载的。而不是直接minIO集群原生提供的下载URL模式的。
     * @param fileName 文件名称
     * @param res response
     */
    public void download(String fileName, HttpServletResponse res) {
        GetObjectArgs objectArgs = GetObjectArgs.builder().bucket(prop.getBucketName())
                .object(fileName).build();
        try (GetObjectResponse response = minioClient.getObject(objectArgs)){
            byte[] buf = new byte[1024];
            int len;
            try (FastByteArrayOutputStream os = new FastByteArrayOutputStream()){
                while ((len=response.read(buf))!=-1){
                    os.write(buf,0,len);
                }
                os.flush();
                byte[] bytes = os.toByteArray();
                res.setCharacterEncoding("utf-8");
                // 设置强制下载不打开
                // res.setContentType("application/force-download");
                res.addHeader("Content-Disposition", "attachment;fileName=" + fileName);
                try (ServletOutputStream stream = res.getOutputStream()){
                    stream.write(bytes);
                    stream.flush();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 查看文件对象
     * @return 存储bucket内文件对象信息
     */
    public List<Item> listObjects() {
        Iterable<Result<Item>> results = minioClient.listObjects(
                ListObjectsArgs.builder().bucket(prop.getBucketName()).recursive(true).build());
        List<Item> items = new ArrayList<>();
        try {
            for (Result<Item> result : results) {
                items.add(result.get());
            }
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        return items;
    }
    /**
     * 转换对象
     * @param items
     * @return
     */
    private List<Item> convert(Iterable<Result<Item>> items) {
        return StreamSupport
                .stream(items.spliterator(), true)
                .map(itemResult -> {
                    try {
                        return itemResult.get();
                    } catch (Exception e) {
                        //throw  MinioExceptionUtil.wapper(e);
                        return null;
                    }
                })
                .collect(Collectors.toList());
    }
    /**
     * 删除
     * @param fileName
     * @return
     * @throws Exception
     */
    public String remove(String bucketName, String fileName) {
        try {
            // 先检查连接状态
            if (!bucketExists(bucketName)) {
                return "MinIO服务不可用或bucket不存在";
            }
            minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(fileName).build());
            return null;
        } catch (Exception e) {
            log.error("MinIO删除操作失败: {}", e.getMessage(), e);
            // 识别连接异常
            if (e.getMessage().contains("Connection refused") ||
                    e.getMessage().contains("ConnectException")) {
                return "MinIO服务连接失败";
            }
            return e.getMessage();
        }
    }
    /**
     * 获取对象流
     * @param bucket
     * @param objectId
     * @param consumer 外部注入 函数
     * @return
     */
    public InputStream getObject(final String bucket, final String objectId, final Consumer<GetObjectArgs.Builder> consumer) {
        final String minioId ="";// MinioUtils.minioId(objectId);
        try {
            final GetObjectArgs.Builder builder = GetObjectArgs.builder()
                    .bucket(bucket)
                    .object(minioId);

            if (consumer != null) {
                consumer.accept(builder);
            }

            final GetObjectArgs args = builder.build();

            return minioClient.getObject(builder.build());
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 获取单个对象状态
     * @param bucket
     * @param objectName
     * @param consumer
     * @return
     */
    public StatObjectResponse statObject(final String bucket, final String objectName,
                                         final Consumer<StatObjectArgs.Builder> consumer) {
        try {
            final StatObjectArgs.Builder builder = StatObjectArgs.builder()
                    .bucket(bucket)
                    .object(objectName);
            if (consumer != null) {
                consumer.accept(builder);
            }
            final StatObjectArgs args = builder.build();
            return minioClient.statObject(args);
        } catch (Exception e) {
//            e.printStackTrace();
            return null;
        }
    }
    /**
     * 获取单个对象元数据
     * @param bucket
     * @param objectId
     * @param consumer
     * @return
     */
    public Map<String, String> getUserMetadata(final String bucket, final String objectId,
                                             final Consumer<StatObjectArgs.Builder> consumer) {
        final StatObjectResponse statObjectResponse = this.statObject(bucket, objectId, consumer);
        if(null==statObjectResponse)    return null;
        else  return statObjectResponse.userMetadata();
    }
    /**测试用的： 对象保留期
     */
    public Map<String, String> getRetention(final String objectName) {
        try {
            // 获取对象保留配置
            Retention retention =
                    minioClient.getObjectRetention(
                            GetObjectRetentionArgs.builder()
                                    .bucket(prop.getBucketName())
                                    .object(objectName)
                                    .build());

            System.out.println("Mode: " + retention.mode());
            System.out.println("Retainuntil Date: " + retention.retainUntilDate());
        } catch (Exception e) {
            //ObjectLock没有开启的：报错Bucket is missing ObjectLockConfiguration,
              e.printStackTrace();
            return null;
        }
        return null;
    }
    /**测试用的： 对象保留期
     */
    public Map<String, String> setupRetention(final String objectName) {
        try {
        // 对象保留配置，保留至当前日期后3天。
        ZonedDateTime retentionUntil = ZonedDateTime.now(Time.UTC).plusDays(1).withNano(0);
        Retention retention1 = new Retention(RetentionMode.COMPLIANCE, retentionUntil);
                minioClient.setObjectRetention(
                        SetObjectRetentionArgs.builder()
                                .bucket(prop.getBucketName())
                                .object(objectName)
                                .config(retention1)
                                .bypassGovernanceMode(true)
                                .build());
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        return null;
    }
    /**
     * 处理普通 HTTP 文件上传到 MinIO
     */
    public String uploadFile(MultipartFile file, User who) {
        try {
            // 生成唯一文件名
            String originalFileName = file.getOriginalFilename();
            String fileExtension = "";
            if (originalFileName != null && originalFileName.contains(".")) {
                fileExtension = originalFileName.substring(originalFileName.lastIndexOf("."));
            }

            String format = PathSdf.format(new Date());
            String objectName = format + UUID.randomUUID().toString() + fileExtension;

            // 设置元数据
            Map<String, String> metas = new HashMap<>();
            metas.put("author", who.getUsername());

            // 检查对象是否已存在
            StatObjectResponse oldStat = this.statObject(prop.getBucketName(), objectName, null);
            if (null != oldStat) {
                throw new RuntimeException("文件已存在: " + objectName);
            }

            // 上传到 MinIO
            PutObjectArgs objectArgs = PutObjectArgs.builder()
                    .bucket(prop.getBucketName())
                    .object(objectName)
                    .stream(file.getInputStream(), file.getSize(), -1)
                    .contentType(file.getContentType())
                    .userMetadata(metas)
                    .build();

            minioClient.putObject(objectArgs);

            // 返回文件访问链接 ID (bucketName/objectName) + "/"
            String fileUrl = prop.getBucketName() + objectName;

            // 验证上传是否成功
            StatObjectResponse newStat = this.statObject(prop.getBucketName(), objectName, null);
            if (null == newStat) {
                throw new RuntimeException("MinIO 上传验证失败: " + objectName);
            }

            return fileUrl;

        } catch (Exception e) {
            throw new RuntimeException("MinIO 上传失败: " + e.getMessage(), e);
        }
    }
    /**
     * 处理普通 HTTP 多文件上传
     */
    public Map<String, String> uploadMultipleFiles(MultipartFile[] files, User who) {
        Map<String, String> results = new HashMap<>();
        for (MultipartFile file : files) {
            if (!file.isEmpty()) {
                try {
                    String fileUrl = uploadFile(file, who);
                    if (fileUrl != null) {
                        results.put(file.getOriginalFilename(), fileUrl);
                    }
                } catch (Exception e) {
                    log.error("文件上传失败: {}", file.getOriginalFilename(), e);
                    // 单个文件失败不影响其他文件
                }
            }
        }
        return results;
    }
    /**
     * 获取 MinioClient 实例
     */
    public MinioClient getMinioClient() {
        return minioClient;
    }
}

/*
Minio走 S3协议方式对接：GO语言开发的。   https://blog.csdn.net/Gefangenes/article/details/130719006
兼容性 s3 存储协议：分布式文件系统，
export AWS_ACCESS_KEY_ID=<access key id>
export AWS_SECRET_ACCESS_KEY=<secret access key>
由于tensorboard内部使用的是默认的s3 endpoint, 是AWS, 所以minio搭建的s3会有问题, 而tensorflow基于的boto3是无法使用环境变量更改endpoint_url的,
所以需要安装这个版本的boto3 ；  CA证书，
然后在环境变量里面设置AWS_ENDPOINT_URL=http://ip:port
* */
